Chapter 11

Forms, Input Handling, and Validation

Session 11

Learning Objectives

By the end of this chapter, you will be able to:

1

Why Forms Matter

Forms are the primary way users provide structured input (signup, login, settings). Correct form handling makes apps feel reliable and reduces user frustration by preventing invalid data and guiding corrections.

2

Form Basics: Form and TextFormField

Understanding the core form widgets is essential for building effective input interfaces.

Core Concepts

  • Wrap related input fields in a Form widget and supply a GlobalKey<FormState> to read and validate state.
  • Use TextFormField for fields that need validation; it integrates with Form via its validator callback.
  • For simple text without validation, TextField is fine.

Core Pattern

final _formKey = GlobalKey();

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        decoration: InputDecoration(labelText: 'Email'),
        validator: (value) {
          if (value == null || value.isEmpty) return 'Email is required';
          if (!value.contains('@')) return 'Enter a valid email';
          return null;
        },
      ),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            // proceed
          }
        },
        child: Text('Submit'),
      ),
    ],
  ),
)
3

Controllers and Focus Management

Controllers and focus nodes provide programmatic control over form fields.

Using Controllers and Focus Nodes

  • Use TextEditingController to read or set field values programmatically and FocusNode to manage focus and keyboard flow.
  • Always dispose controllers and focus nodes in dispose() of a StatefulWidget.

Example

late TextEditingController _emailController;
late FocusNode _emailFocus;

@override
void initState() {
  super.initState();
  _emailController = TextEditingController();
  _emailFocus = FocusNode();
}

@override
void dispose() {
  _emailController.dispose();
  _emailFocus.dispose();
  super.dispose();
}

Focus flow example: move to next field on "next" action

TextFormField(
  controller: _emailController,
  textInputAction: TextInputAction.next,
  onFieldSubmitted: (_) => FocusScope.of(context).requestFocus(_passwordFocus),
)
4

Validation Strategies

Effective validation improves user experience and data quality.

Validation Types

  • Synchronous validators: Quick checks (required, length, pattern) implemented in validator.
  • Asynchronous validation: Server-side uniqueness checks (username/email) performed separately; do not block UI; show inline loading or delayed validation results.
  • Debounce network validation to avoid excessive calls.

Validation UX Rules

  • Validate required fields on submit; optionally show real-time suggestions for long forms.
  • Show one clear error per field and place it close to the input.
  • Use accessible language and actionable messages ("Password must be at least 8 characters" rather than "Invalid password").

Asynchronous Pattern (Conceptual)

On field change, start a debounce timer; after delay, call API; show spinner icon; if response indicates invalid, set field-level error state (use setState or a form-level error map).

5

Common Validators and Utilities

Reusable validators make form validation consistent and maintainable.

Common Validators

  • Required: Check null/empty.
  • Email: Lightweight regex or package email_validator. Don't overcomplicate regex; accept most valid emails.
  • Password strength: Length, character classes, and optionally entropy estimators.
  • Numeric range: Parse with int.tryParse/double.tryParse and verify bounds.
  • Cross-field validation: E.g., confirm password must match password — check both controllers in form submit or via custom validator that accesses other field values.

Cross-Field Example

String? confirmValidator(String? value) {
  if (value != _passwordController.text) return 'Passwords do not match';
  return null;
}
6

Input Types, Keyboard, and Formatting

Proper input configuration improves user experience and data quality.

Input Configuration

  • Use keyboardType to show appropriate keyboard: TextInputType.emailAddress, TextInputType.number, TextInputType.phone, etc.
  • Use TextInputAction to control action button (next, done).
  • Use input formatters (FilteringTextInputFormatter, TextInputFormatter) to restrict characters (phone numbers, numeric-only).
  • For structured inputs (dates, currencies), prefer specialized pickers or format/display separate from raw input to avoid parsing errors.

Example Numeric Input

TextFormField(
  keyboardType: TextInputType.number,
  inputFormatters: [FilteringTextInputFormatter.digitsOnly],
)
7

Accessibility and Internationalization

Making forms accessible and internationalized ensures they work for all users.

Best Practices

  • Provide labelText and hintText in InputDecoration. Use semanticsLabel for non-text visual inputs.
  • Respect input locale for number and date formatting using Intl or platform APIs.
  • Ensure error messages are announced by screen readers (use autovalidateMode carefully; avoid noisy live validation for screen reader users).
8

Submit Flows and Progressive Disclosure

Well-designed submit flows improve user experience and reduce errors.

Submit Best Practices

  • Avoid blocking the entire UI during network calls; show per-button or per-field loading states.
  • Disable the submit button when the form is invalid or a submission is in progress.
  • For long forms, use multi-step forms to reduce cognitive load and save progress locally (SharedPreferences) or server-side drafts.

Submit Pattern

bool _submitting = false;

ElevatedButton(
  onPressed: _submitting ? null : _submit,
  child: _submitting ? CircularProgressIndicator() : Text('Submit'),
)
9

Security and Sanitization

Security is critical when handling user input.

Security Best Practices

  • Never trust client-side validation; always validate on the server.
  • Sanitize inputs before sending to avoid injection or invalid payloads. For text fields, trim whitespace and normalize unicode if needed.
  • Avoid storing sensitive data like raw passwords in local persistent storage. Use secure storage for tokens only.

Sanitization Example

final email = _emailController.text.trim().toLowerCase();
10

Handling File and Image Inputs

File and image inputs require special handling for good UX.

File Input Handling

  • Use packages like image_picker to obtain images. Show image preview and validate file size/type before upload.
  • For large files, upload in background with progress and resumable logic where possible.

UX tip: Compress or resize images on-device before upload to reduce bandwidth and improve perceived performance.

11

Testing Forms

Testing ensures forms work correctly and handle edge cases.

Testing Strategies

  • Unit-test validators as pure functions returning expected errors or null.
  • Widget tests: pump the form, enter text via tester.enterText and trigger submit; assert validation messages and submission behavior.
  • Integration tests: exercise full submit flows including network mocks.

Example Validator Unit Test

  • assert emailValidator('') == 'Email is required'
  • assert emailValidator('bad') == 'Enter a valid email'
  • assert emailValidator('a@b.com') == null
12

Practical Examples (Patterns to Copy)

Common form patterns you can adapt for your applications.

Signup Flow

Name, email, password, confirm password. Validate email format and password strength, confirm password matches, and call async API to create account. Show inline loading for email uniqueness check.

Edit Profile

Prepopulate controllers from model, allow changing fields, and enable Save only when something changed (dirty-check comparing controllers to initial values).

Search Box

Use TextField with debounce to call search API; show spinner while fetching and cancel previous requests when new query arrives.

13

Exercises

Practice what you've learned with these exercises:

1. Login form

Build a login screen with email and password fields, TextEditingControllers, a toggle to show/hide password, validation, and submit button that simulates a network call (use Future.delayed). Disable submit while submitting and show a success message on completion.

2. Signup with confirm password and async username check

Create a signup form with username, email, password, confirm password. Validate fields synchronously and simulate an asynchronous username uniqueness check with a 500ms debounce. Show appropriate inline error messages.

3. Multi-step profile form

Implement a 2-step form: Step 1 (personal details), Step 2 (photo and preferences). Save intermediate state locally so the user can resume if the app restarts.

4. Unit-test validators

Extract validators as pure functions and write unit tests for each edge case (empty, invalid format, boundary values).

14

Session Assignment

Complete Exercises 1 and 2. Include code, screenshots, and a short README explaining validation choices, debounce implementation, and how you handled controller disposal and focus transitions.